﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.Serialization.Formatters.Binary;
using System.Web;
using System.Web.Services;
using System.Web.Script.Services;
using System.Web.UI;
using GISAzureCore;
using GISCore.Geometry;
using GISCore.Geometry.IO;
using GISCore.Geometry.Merge;
using GISCore.Geometry.Partition;
using Microsoft.WindowsAzure;
using Microsoft.WindowsAzure.ServiceRuntime;
using Microsoft.WindowsAzure.StorageClient;

namespace GISWebRole
{
    public partial class _Default : System.Web.UI.Page
    {
        private enum BalancingScheme { None, LargestFirst, RowOrder, ColumnOrder }

        private static class SchemeGenerator
        {
            /// <summary>
            /// this will generate a load balancing scheme function 
            /// based on the scheme enum requested
            /// </summary>
            /// <param name="scheme">the balancing scheme to generate</param>
            /// <returns>a balancing scheme</returns>
            internal static Func<IEnumerable<KeyValuePair<int, ICellable>>, IEnumerable<ICellable>>
                GenerateScheme(BalancingScheme scheme)
            {
                Func<IEnumerable<KeyValuePair<int, ICellable>>, IEnumerable<ICellable>> func;
                switch (scheme)
                {
                    case BalancingScheme.None:
                        func = (cells) => from pair in cells select pair.Value;
                        break;
                    case BalancingScheme.LargestFirst:
                        func = (cells) =>
                        {
                            var cellist = cells.ToList();
                            cellist.Sort();
                            return cellist as IEnumerable<ICellable>;
                        };
                        break;
                    default:
                        func = (cells) => from pair in cells select pair.Value;
                        break;
                }
                return func;
            }
        }

        private class JobItem
        {
            internal int CellRowCount;
            internal int CellColumnCount;
            internal string Result;
            internal string Map1;
            internal string Map2;
            internal OverlayOp Op;
            internal BalancingScheme Scheme;

            internal JobItem(int cellRows, int cellCols, string map1, string map2,
                OverlayOp op, BalancingScheme scheme)
            {
                CellRowCount = cellRows;
                CellColumnCount = cellCols;
                Map1 = map1;
                Map2 = map2;
                Op = op;
                Scheme = scheme;
            }

            public override string ToString()
            {
                string op = Enum.GetName(typeof(OverlayOp), Op);
                string sch = Enum.GetName(typeof(BalancingScheme), Scheme);
                return string.Format("Map {0} {1} Map {2}: {3} x {4} by {5}", Map1, op, Map2, CellRowCount, CellColumnCount, sch);
            }
        }

        protected void Page_Load(object sender, EventArgs e)
        {           
            if (!IsPostBack)
            {
                CloudStorageAccount account = CloudStorageAccount.Parse(
                   RoleEnvironment.GetConfigurationSettingValue("DataConnectionString"));
                CloudBlobClient blobClient = account.CreateCloudBlobClient();
                _setupDropDownLists(blobClient);
            }
        }

        /// <summary>
        /// Bind the drop down lists to the blob-maps and enumerations
        /// </summary>
        /// <param name="blobClient"></param>
        private void _setupDropDownLists(CloudBlobClient blobClient)
        {
            CloudBlobContainer container = blobClient.GetContainerReference(DataConstants.MapContainerBlob);
            CloudBlobDirectory dir = container.GetDirectoryReference("./");

            List<string> maps = (from map in dir.ListBlobs()
                                 select Path.GetFileName(map.Uri.AbsolutePath)).ToList();

            List<string> maps2 = (from map in maps select map).ToList();

            Array ops = Enum.GetValues(typeof(OverlayOp));
            Array schemes = Enum.GetValues(typeof(BalancingScheme));

            this.ddlMap1.DataSource = maps;
            this.ddlMap2.DataSource = maps2;
            this.ddlOperator.DataSource = ops;
            this.ddlLoadBalance.DataSource = schemes;

            this.ddlMap1.DataBind();
            this.ddlMap2.DataBind();
            this.ddlOperator.DataBind();
            this.ddlLoadBalance.DataBind();
        }

        [WebMethod()]
        [ScriptMethod()]
        public static string Overlay(string map1, string map2, string op, string scheme, string col, string  row)
        {            
            CloudStorageAccount account = CloudStorageAccount.Parse(
                  RoleEnvironment.GetConfigurationSettingValue("DataConnectionString"));

            int messageAmt;
            string msgConfAmt = RoleEnvironment.GetConfigurationSettingValue("MessageAmount");

            if (!int.TryParse(msgConfAmt, out messageAmt))
            {
                messageAmt = 2;
            }
            JobItem item = null;
            string result = string.Empty;
            try
            {
                item = new JobItem(int.Parse(row), int.Parse(col), map1, map2,
                (OverlayOp)Enum.Parse(typeof(OverlayOp), op), (BalancingScheme)Enum.Parse(typeof(BalancingScheme), scheme));
                result = _overlay(messageAmt, item, account);
            }
            catch (FormatException)
            {
                result = "Invalid parameter format";
            }
            catch (ArgumentNullException)
            {
                result = "Parameters cannot be null";
            }
            catch (ArgumentException)
            {
                result = "Invalid parameter format";
            }
            catch (Exception e)
            {
                result = e.Source;
            }
            return result;
        }

        private static string _overlay(int messageAmt, JobItem item, CloudStorageAccount act)
        {
            string result = string.Empty;
            Stopwatch sw = new Stopwatch();
            CloudBlobClient blobClient = act.CreateCloudBlobClient();
            CloudBlobContainer mapContainer = blobClient.GetContainerReference(DataConstants.MapContainerBlob);
            CloudBlobContainer sweepItemContainer = blobClient.GetContainerReference(DataConstants.SweepItemContainerBlob);
            CloudQueueClient queueClient = act.CreateCloudQueueClient();
            CloudQueue sweepItemQueue = queueClient.GetQueueReference(DataConstants.SweepItemQueue);
            CloudQueue mergeItemQueue = queueClient.GetQueueReference(DataConstants.MergeItemQueue);
            sweepItemQueue.Clear();
            mergeItemQueue.Clear();

            try
            {
                // retrieve the maps
                CloudBlob map1 = mapContainer.GetBlockBlobReference(item.Map1);
                CloudBlob map2 = mapContainer.GetBlockBlobReference(item.Map2);

                // get our load balancing scheme
                var scheme = SchemeGenerator.GenerateScheme(item.Scheme);

                System.Diagnostics.Trace.WriteLine(string.Format("GISWebRole Submitting Job {0}", item), "Information");

                // partition the data and send cell items to workers then merge results into new layer
                sw.Start();
                IDictionary<int, ICellable> cells = _createAndSendSweepItems(sweepItemContainer, sweepItemQueue, map1, map2, scheme, item);

                System.Diagnostics.Trace.WriteLine(string.Format("Created {0} cells", cells.Count), "Information");
                System.Diagnostics.Trace.WriteLine(string.Format("Merging results", item), "Information");

                Layer layer = _mergeResults(messageAmt, sweepItemContainer, mergeItemQueue, cells.Count, item);
                sw.Stop();

                // output the new layer
                //IFileHandler handler = new GMLFileHandler();
                //using (BlobStream output = mapContainer.GetBlobReference("output.gml").OpenWrite())
                //{
                //    handler.Write(layer, output);
                //}

                result = string.Format("Read, Sweep, and Collect Time: {0}", sw.Elapsed);
                System.Diagnostics.Trace.WriteLine(string.Format("GISWebRole Completed Job in {0}", sw.Elapsed), "Information");
            }
            catch (Exception exp)
            {
                System.Diagnostics.Trace.WriteLine(exp.Message, "Error");
                throw;
            }
            finally
            {
                sweepItemQueue.Clear();
                mergeItemQueue.Clear();
            }
            return result;
        }
        
        /// <summary>
        /// Create the necessary GIS objects and begin pushing data into the table
        /// As well as sending messages to workers
        /// </summary>
        /// <param name="sweepItemContainer"></param>
        /// <param name="map1"></param>
        /// <param name="map2"></param>
        /// <param name="scheme"></param>
        private static IDictionary<int,ICellable> _createAndSendSweepItems(CloudBlobContainer sweepItemContainer, CloudQueue sweepItemQueue, 
            CloudBlob map1, CloudBlob map2, Func<IEnumerable<KeyValuePair<int, ICellable>>, IEnumerable<ICellable>> scheme, JobItem item)
        {            
            Layer layer1 = null, layer2 = null;
            System.Threading.EventWaitHandle[] handles = new System.Threading.EventWaitHandle[2];
            handles[0] = new System.Threading.EventWaitHandle(false, System.Threading.EventResetMode.AutoReset);
            handles[1] = new System.Threading.EventWaitHandle(false, System.Threading.EventResetMode.AutoReset);          

            //read the two maps in parallel
            System.Threading.ThreadPool.QueueUserWorkItem((obj) =>
                {
                    using (BlobStream stream = map1.OpenRead())
                    {
                        IFileHandler handler = new GMLFileHandler(0);
                        layer1 =  handler.Read(stream);
                        handles[0].Set();
                    }
                });

            System.Threading.ThreadPool.QueueUserWorkItem((obj) =>
                {
                    using (BlobStream stream = map2.OpenRead())
                    {
                        IFileHandler handler = new GMLFileHandler(1);
                        layer2 = handler.Read(stream);
                        handles[1].Set();
                    }
                });

            //wait for the two maps to be fully read and instantiated
            handles[0].WaitOne();
            handles[1].WaitOne();

            List<Layer> layers = new List<Layer>() { layer1, layer2 };
            IPartitionable plane = new UniformGrid(item.CellRowCount, item.CellColumnCount);
            var cells = plane.Partition(layer1, layer2);
            TimeSpan ts = new TimeSpan(1, 0, 0);
            foreach (var cell in scheme(cells))
            {
                string id = Guid.NewGuid().ToString();
                var blob = sweepItemContainer.GetBlobReference(id);
                using (BlobStream stream = blob.OpenWrite())
                {
                    BinaryFormatter bf = new BinaryFormatter();
                    bf.Serialize(stream, cell);
                }
                sweepItemQueue.AddMessage(new CloudQueueMessage(id), ts);
            }

            return cells;
        }

        /// <summary>
        /// Merge the data processed by the workers
        /// </summary>
        /// <param name="cellCtxt"></param>
        /// <param name="joinQueue"></param>
        /// <param name="cells"></param>
        /// <returns></returns>
        private static Layer _mergeResults(int messageAmt, CloudBlobContainer sweepItemContainer, CloudQueue joinQueue, int cellCount, JobItem item)
        {
            int processed = 0;
            IMergable merger = new LinkedMerge();
            var cellTable = new Dictionary<int, Cell>();
            while (processed < cellCount)
            {
                IEnumerable<CloudQueueMessage> messages = joinQueue.GetMessages(messageAmt, TimeSpan.FromMinutes(30.0));

                foreach (var msg in messages)
                {
                    int id = int.Parse(msg.AsString);
                    var blob = sweepItemContainer.GetBlobReference(msg.AsString);
                    using (BlobStream stream = blob.OpenRead())
                    {
                        BinaryFormatter bf = new BinaryFormatter();
                        Cell cell = bf.Deserialize(stream) as Cell;
                        cellTable[id] = cell;
                    }
                    blob.Delete();
                    processed++;
                    joinQueue.DeleteMessage(msg);
                }
            }
            return merger.merge(cellTable);
        }
    }
}
